/*******************************************************************************
 * Copyright (c) 2009 David Harrison.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the GNU Public License v3.0
 * which accompanies this distribution, and is available at
 * http://www.gnu.org/licenses/gpl-3.0.html
 *
 * Contributors:
 *     David Harrison - initial API and implementation
 ******************************************************************************/
package com.sfs.postcodes.dao;

import com.sfs.postcodes.beans.ConfigurationBean;
import com.sfs.postcodes.beans.GeocodeBean;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.StringTokenizer;

import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;

/**
 * The Class GeocoderDAOImpl.
 *
 * @author David Harrison
 */

public class GeocoderDAOImpl implements GeocoderDAO {

    /** The logger. */
    private Logger logger = Logger.getLogger(GeocoderDAO.class);

    /** The _encoding. */
    private final String encoding = "UTF-8";

    /** The _configuration. */
    private ConfigurationBean configuration = null;

    /** Status okay code. */
    private static final int STATUS_OK = 200;

    /** Status unknown error code. */
    private static final int UNKNOWN_ERROR = 500;

    /** Status missing query code. */
    private static final int MISSING_QUERY = 601;

    /** Bad request code. */
    private static final int BAD_REQUEST = 400;

    /** Null code. */
    private static final int NULL_CODE = 602;

    /** Legal problem code. */
    private static final int LEGAL_PROBLEM = 603;

    /** No route code. */
    private static final int NO_ROUTE = 604;

    /** Bad key code. */
    private static final int BAD_KEY = 610;

    /** Too many queries code. */
    private static final int TOO_MANY_QUERIES = 620;

    /** Characters to include in the response substring. */
    private static final int SUBSTRING_COUNTER = 3;


    /**
     * Sets the configuration bean for this GeocoderDAO instance.
     *
     * @param configurationBean the new configuration bean
     */
    public final void setConfiguration(final ConfigurationBean configurationBean) {
        this.configuration = configurationBean;
    }

    /**
     * Gets the configuration for this GeocoderDAO instance.
     *
     * @return the configuration bean
     */
    protected final ConfigurationBean getConfiguration() {
        return this.configuration;
    }


    /**
     * Perform a geocode lookup based on the supplied address, state and country
     * information. Returns a GeocodeBean if a valid code is found, if not null
     * is returned.
     *
     * @param address the address
     * @param state the state
     * @param country the country
     *
     * @return the geocode bean
     *
     * @throws GeocodeLookupException the geocode lookup exception
     * @throws IOException Signals that an I/O exception has occurred.
     */
    public final GeocodeBean findGeocode(final String address, final String state,
            final String country) throws GeocodeLookupException, IOException {

        GeocodeBean geocode = null;

        ArrayList<String> addressArray = new ArrayList<String>();

        // Replace any instances of a carriage return with a comma
        String addressString = StringUtils.replace(address, "\n", ",");

        logger.debug("Address string: " + addressString);

        StringTokenizer st = new StringTokenizer(addressString, ",");
        while (st.hasMoreTokens()) {
            String component = st.nextToken().trim();
            addressArray.add(component);
        }

        logger.debug("Address array size: " + addressArray.size());

        for (int a = 0; a < addressArray.size(); a++) {

            // If a geocode has been found do not perform the search
            if (geocode == null) {
                logger.debug("Geocode is null");

                StringBuffer sb = new StringBuffer();
                // Build search string
                for (int i = 0; i < addressArray.size(); i++) {

                    final int key = i + a;

                    if (key < addressArray.size()) {

                        if (sb.length() > 0) {
                            sb.append(",");
                        }
                        String searchComponent = addressArray.get(key).trim();
                        searchComponent = StringUtils.replace(
                                searchComponent, "P.O. ", "PO ");
                        searchComponent = StringUtils.replace(
                                searchComponent, "P.O.", "PO ");
                        searchComponent = StringUtils.replace(
                                searchComponent, "Post Office ", "PO ");
                        searchComponent = StringUtils.replace(
                                searchComponent, "R.D. ", "R D ");
                        searchComponent = StringUtils.replace(
                                searchComponent, "R.D.", "R D ");
                        searchComponent = StringUtils.replace(
                                searchComponent, "Rural Delivery ", "R D ");

                        sb.append(searchComponent);

                        logger.debug("Search component: " + searchComponent);
                    }
                }
                if (StringUtils.isNotBlank(state)) {
                    sb.append(",");
                    sb.append(state);
                }
                if (StringUtils.isNotBlank(country)) {
                    sb.append(",");
                    sb.append(country);
                }

                // If the search is not an empty string perform the lookup
                if (sb.length() > 0) {
                    geocode = this.performGeocodeLookup(sb.toString());
                }
            }
        }
        return geocode;
    }


    /**
     * Perform geocode lookup.
     *
     * @param search the search
     * @return the geocode bean
     * @throws GeocodeLookupException the geocode lookup exception
     * @throws IOException Signals that an I/O exception has occurred.
     */
    private GeocodeBean performGeocodeLookup(final String search)
            throws GeocodeLookupException, IOException {

        GeocodeBean geocode = null;

        final String url = getConfiguration().getGeocodeUrl();
        final String key = getConfiguration().getGeocodeApiKey();
        final String keyParam = getConfiguration().getGeocodeKeyParam();
        final String addressParam = getConfiguration().getGeocodeAddressParam();

        URL geocodeURL = null;
        try {
            geocodeURL = new URL(url + "&" + addressParam + "="
                + URLEncoder.encode(search, this.encoding)
                + "&" + keyParam + "=" + key);
        } catch (MalformedURLException mue) {
            throw new GeocodeLookupException("Error constructing geocode URL: "
                    + mue.getMessage());
        } catch (UnsupportedEncodingException uee) {
            throw new GeocodeLookupException("Unsupported encoding of geocode: "
                    + uee.getMessage());
        }

        int statusCode = -1;

        BufferedReader in = new BufferedReader(
                new InputStreamReader(geocodeURL.openStream()));
        String line;
        while ((line = in.readLine()) != null) {
            // Format: 200,6,42.730070,-73.690570
            statusCode = Integer.parseInt(line.substring(0, SUBSTRING_COUNTER));
            if (statusCode == STATUS_OK) {
                geocode = new GeocodeBean();
                geocode.setLatitude(
                        line.substring("200,6,".length(),
                        line.indexOf(',', "200,6,".length())));
                geocode.setLongitude(
                        line.substring(line.indexOf(',', "200,6,".length()) + 1,
                        line.length()));
            }
        }
        if (geocode == null) {
            switch (statusCode) {
                case BAD_REQUEST:
                    throw new GeocodeLookupException("Bad Request");
                case UNKNOWN_ERROR:
                    throw new GeocodeLookupException("Unknown Google error");
                case MISSING_QUERY:
                    throw new GeocodeLookupException("Missing query");
                case NULL_CODE:
                    return null;
                case LEGAL_PROBLEM:
                    throw new GeocodeLookupException("Legal problem");
                case NO_ROUTE:
                    throw new GeocodeLookupException("No route");
                case BAD_KEY:
                    throw new GeocodeLookupException("Bad key");
                case TOO_MANY_QUERIES:
                    throw new GeocodeLookupException("Too many queries");
                default:
                    break;
            }
        }

        if (geocode != null) {
            if (StringUtils.isBlank(geocode.getCoordinates())) {
                // No actual geocode found, return null
                geocode = null;
            }
        }
        return geocode;
    }
}
